package org.dftproject.pgv.core.racp;

import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dftproject.gedcom.core.AbstractGedcom;
import org.dftproject.gedcom.core.GedcomPerson;
import org.dftproject.gedcom.core.GedcomReader;
import org.dftproject.gedcom.core.IGedcom;
import org.dftproject.gedcom.core.Line;
import org.dftproject.genesis.core.http.HttpManager;
import org.dftproject.genesis.core.persistence.IMemento;
import org.dftproject.genesis.data.genealogy.GenealogyConstants;
import org.dftproject.genesis.data.genealogy.ISource;
import org.dftproject.genesis.data.genealogy.impl.SourceImpl;
import org.dftproject.genesis.data.impl.AttributeImpl;
import org.dftproject.pgv.core.cache.CacheManager;
import org.dftproject.pgv.core.racp.factories.PGVGedcomFactory;

public class PGVGedcom extends AbstractGedcom implements IGedcom {

	private static final Log log = LogFactory.getLog(PGVGedcom.class);
	
	private final static Pattern tabbedLinePattern = Pattern.compile("^([^\\t]*)\\t([^\\t]*)$");

	private final Session session;
	private final String gedcomId;
	private String title;

	public PGVGedcom(Session session, String gedcomId) {
		this(session, gedcomId, null);
	}

	public PGVGedcom(Session session, String gedcomId, String title) {
		this.session = session;
		this.gedcomId = gedcomId;
		this.title = title;
	}

	public String getUrl() {
		return session.getUrl();
	}

	public Session getSession() {
		return session;
	}

	public String getGedcomId() {
		return gedcomId;
	}

	public String getTitle() {
		return title;
	}
	
	protected void initTitle() {
		String response = HttpManager.getDefault().getWebPage(session.getUrl() + "client.php?action=listgedcoms&" + session.getSessionComponent());
		if (response != null) {
			String[] lines = response.split("\n");
			if (lines.length > 1 && "SUCCESS".equals(lines[0]) && !lines[1].startsWith("<"))	{
				for (String line : lines) {
					Matcher matcher = tabbedLinePattern.matcher(line);
					if (matcher.matches() && gedcomId.equals(matcher.group(1))) {
						title = matcher.group(2);
					}
				}
			}
		}
	}

	public String getGedcomUrl() {
		try {
			return getUrl() + "index.php?ged=" + URLEncoder.encode(getGedcomId(), "UTF-8");
		} catch (UnsupportedEncodingException e) {
			return null;
		}
	}

	@Override
	public GedcomPerson getPerson(String id) {
		Line record = getRecord(id);
		if (record != null && "INDI".equals(record.getTag()))
			return new PGVPerson(this, id, record);
		return null;
	}

	public String getPersonUrl(String id) {
		try {
			return getUrl() + "individual.php?pid=" + id + "&ged=" + URLEncoder.encode(getGedcomId(), "UTF-8");
		} catch (UnsupportedEncodingException e) {
			return null;
		}
	}

	/**
	 * Returns the record associated with <code>id</code>.
	 * @return the record
	 */
	public Line getRecord(String id) {
		Line[] lines = getRecords(new String[] { id });
		if (lines.length > 0) {
			if (log.isDebugEnabled())
				log.debug(lines[0].toString(true));
			return lines[0];
		}
		log.error("Failed to get record for " + id);
		return null;
	}

	/**
	 * Returns the records associated with the <code>ids</code>.
	 * @param ids an array of record ids
	 * @return an array of records
	 */
	public Line[] getRecords(String[] ids) {
		if (!session.isValid() && !session.connect())
			return new Line[0];

		List<Line> lines = new LinkedList<Line>();

		// First try to get the record from the cache

		CacheManager cacheManager = CacheManager.getDefault();
		Date today = getToday();

		Set<String> xrefs = new HashSet<String>(Arrays.asList(ids));
		for (String xref : ids) {
			// The cache must have a retrieval date for a record

			Date retrievalDate = cacheManager.getRetrievalDate(getUrl(), gedcomId, xref);
			if (retrievalDate == null)
				continue;

			// The retrieval date must be on or after today

			if (!retrievalDate.after(today) && !retrievalDate.equals(today)) {
				// If not, we must have request a list of changes today

				Date lastGetChanges = cacheManager.getLastGetChanges(getUrl(), gedcomId);
				if (lastGetChanges != null && !lastGetChanges.after(today) && !lastGetChanges.equals(today)) {
					// If not, request the list of changes and invalidate old
					// records

					Set<String> changedXrefs = getChanges(lastGetChanges, session.getSessionComponent());
					cacheManager.invalidateRecords(getUrl(), gedcomId, today, changedXrefs);

					// This record must not have changed since the retrieval
					// date

					if (changedXrefs.contains(xref)) {
						log.info(xref + " has changed since " + new SimpleDateFormat("d MMM yyyy").format(retrievalDate));
						continue;
					}
				}
			}

			// Get the record from the cache

			String record = cacheManager.getRecord(getUrl(), gedcomId, xref);
			if (record != null) {
				xrefs.remove(xref);

				try {
					GedcomReader reader = new GedcomReader();
					lines.addAll(reader.read(new StringReader(record)));
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}

		// Request all cache misses
		// TODO Batch record requests with one RACP call and split the results

		for (String xref : xrefs) {
			Date retrieved = new Date();
			String data = getRecords(new String[] { xref }, session.getSessionComponent());
			if (data != null) {
				cacheManager.putRecord(getUrl(), gedcomId, xref, data, retrieved);

				try {
					GedcomReader reader = new GedcomReader();
					lines.addAll(reader.read(new StringReader(data)));
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}

		return lines.toArray(new Line[0]);
	}

	/**
	 * Returns a {@link Date} representing the beginning of today.
	 * @return a date
	 */
	protected Date getToday() {
		Calendar today = Calendar.getInstance();
		today.set(Calendar.HOUR_OF_DAY, 0);
		today.set(Calendar.MINUTE, 0);
		today.set(Calendar.SECOND, 0);
		today.set(Calendar.MILLISECOND, 0);
		return today.getTime();
	}

	/**
	 * Returns the data for the requested records.
	 * @param xrefs an array of xrefs
	 * @param sessionComponent the session component
	 * @return the data for the requested records, or null on failure
	 */
	protected String getRecords(String[] xrefs, String sessionComponent) {
		if (xrefs == null || xrefs.length == 0 || sessionComponent == null)
			return null;

		StringBuilder xrefParameter = new StringBuilder();
		for (String xref : xrefs)
			xrefParameter.append(xref);

		String contents= null;
		try {
			contents = HttpManager.getDefault().getWebPage(getUrl() + "client.php?action=get&GEDCOM=" + URLEncoder.encode(gedcomId, "UTF-8") + "&xref=" + xrefParameter + "&" + sessionComponent);
		} catch (UnsupportedEncodingException e) {
			log.error("Failed to encode HTTP query parameter as UTF-8", e);
		}
		if (contents == null)
			return null;

		String[] lines = contents.split("\n");
		if (lines.length < 2)
			return null;

		if ("SUCCESS".equals(lines[0]) && !lines[1].startsWith("<")) {
			StringBuilder data = new StringBuilder();
			for (int i = 1; i < lines.length; i++) {
				data.append(lines[i]);
				if (i < lines.length - 1)
					data.append("\n");
			}

			return data.toString();
		}

		return null;
	}

	/**
	 * Returns a set of xrefs for records that have changed since
	 * <code>date</code>.
	 * @param date a date
	 * @param sessionComponent the session component
	 * @return a set of xrefs for records that have changed
	 */
	protected Set<String> getChanges(Date date, String sessionComponent) {
		if (date == null || sessionComponent == null)
			return Collections.emptySet();

		String contents = HttpManager.getDefault().getWebPage(getUrl() + "client.php?action=getchanges&GEDCOM=" + gedcomId + "&date=" + date + "&" + sessionComponent);
		if (contents == null)
			return Collections.emptySet();

		String[] lines = contents.split("\n");
		if (lines.length < 2)
			return Collections.emptySet();

		if ("SUCCESS".equals(lines[0]) && !lines[1].startsWith("<")) {
			Set<String> xrefs = new HashSet<String>();
			for (String line : lines) {
				if (!"".equals(line.trim()))
					xrefs.add(line);
			}

			return xrefs;
		}

		return Collections.emptySet();
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((gedcomId == null) ? 0 : gedcomId.hashCode());
		result = prime * result + ((session == null) ? 0 : session.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		final PGVGedcom other = (PGVGedcom) obj;
		if (gedcomId == null) {
			if (other.gedcomId != null)
				return false;
		} else if (!gedcomId.equals(other.gedcomId))
			return false;
		if (session == null) {
			if (other.session != null)
				return false;
		} else if (!session.equals(other.session))
			return false;
		return true;
	}
	
	public ISource asSource(String id) {
		SourceImpl source = new SourceImpl();
		source.addAttribute(new AttributeImpl(GenealogyConstants.type, "org.dftproject.pgv.source"));
		if (title == null)
			initTitle();
		if (title != null)
			source.addAttribute(new AttributeImpl(GenealogyConstants.title, title));
		source.addAttribute(new AttributeImpl(GenealogyConstants.collection, this));
		source.addAttribute(new AttributeImpl("id", id));
		return source;
	}

	public String getFactoryId() {
		return PGVGedcomFactory.ID;
	}

	public void saveState(IMemento memento) {
		memento.putString("url", session.getUrl());
		memento.putString("gedcomId", gedcomId);
	}

}
